/*
 * Famon (http://code.google.com/p/famon/)
 * Copyright (c) 2010 Carl Crous.
 *
 * This file is part of Famon.
 *
 * Famon is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Famon is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Famon.  If not, see <http://www.gnu.org/licenses/>.
 */

package za.org.foobar.famon.collector;

import com.google.appengine.api.datastore.Key;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import za.org.foobar.famon.dto.CLoggingEvent;
import za.org.foobar.famon.dto.CStackTraceElement;
import za.org.foobar.famon.model.ApplicationVersion;
import za.org.foobar.famon.model.SourceLocation;
import za.org.foobar.famon.util.DB;
import za.org.foobar.famon.util.DBQuery;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;


/** This class where SourceLocation entries are retrieved.
 *
 * This class caches as much as it can.
 */
public class

        SourceLocationStore {
    private final Log log = LogFactory.getLog(getClass());
    private static final long LIMIT_PER_QUERY = 128;
    private HashMap<String,SourceLocation> sourceLocations = new HashMap<String,SourceLocation>();
    private HashMap<Key, CacheList> cacheLists = new HashMap<Key, CacheList>();
    private List<SourceLocationEntry> entriesToCache = new ArrayList<SourceLocationEntry>();

    private static SourceLocationStore instance;
    public static SourceLocationStore get() {
        return instance == null ? instance = new SourceLocationStore() : instance;
    }
    
    public SourceLocationStore() {
        log.debug("Creating SourceLocationStore");
    }

    public SourceLocation getSourceLocation(ApplicationVersion applicationVersion, CStackTraceElement dto) {
        return getSourceLocation(applicationVersion, dto.getFileName(), dto.getLineNumber(), dto.getClassName(),
                dto.getMethodName());
    }

    public SourceLocation getSourceLocation(ApplicationVersion applicationVersion, CLoggingEvent dto) {
        return getSourceLocation(applicationVersion, dto.getFileName(), dto.getLineNumber(), dto.getClassName(),
                dto.getMethodName());
    }

    public SourceLocation getSourceLocation(ApplicationVersion applicationVersion, String fileName, int lineNumber,
                                            String className, String methodName) {
        String key = SourceLocation.getHashKey(applicationVersion, fileName, lineNumber, className, methodName);
        return sourceLocations.get(key);
    }

    public void createEntries() {
        Collections.sort(entriesToCache);
//        if (log.isDebugEnabled()) {
//            //log.debug("Creating Entries:");
//            for (SourceLocationEntry e : entriesToCache) {
//                //log.debug(e.key);
//            }
//            //log.debug("//////////////////////////////////////////////////////-\n");
//        }
        for (SourceLocationEntry e : entriesToCache) {
            //log.debug("========================================================================");
            addSourceLocation(e);
            //log.debug("========================================================================");
        }
        entriesToCache.clear();
        for (SourceLocation sourceLocation : sourceLocations.values()) {
            if (sourceLocation.getId() == null) {
                log.debug("Persisting new SourceLocation: " + sourceLocation.getFileLine());
                DB.persist(sourceLocation);
            }
        }
    }

    public void addSourceLocation(ApplicationVersion applicationVersion, CStackTraceElement dto) {
        entriesToCache.add(new SourceLocationEntry(applicationVersion, dto.getFileName(), dto.getLineNumber(),
                dto.getClassName(), dto.getMethodName()));
    }

    public void addSourceLocation(ApplicationVersion applicationVersion, CLoggingEvent dto) {
        entriesToCache.add(new SourceLocationEntry(applicationVersion, dto.getFileName(), dto.getLineNumber(),
                dto.getClassName(), dto.getMethodName()));
    }

    public void addSourceLocation(ApplicationVersion applicationVersion, String fileName, int lineNumber,
                                            String className, String methodName) {
        entriesToCache.add(new SourceLocationEntry(applicationVersion, fileName, lineNumber, className, methodName));
    }

    private void addSourceLocation(SourceLocationEntry e) {

        // Check if the cache has our value
        //log.debug("Looking for " + e.key);
        // Check if we have cached the value
        SourceLocation sourceLocation = sourceLocations.get(e.key);
        if (sourceLocation != null) {
            //log.debug("Found in cache");
            return;
        }

        Key versionId = e.applicationVersion.getId();
        CacheList cacheList = cacheLists.get(versionId);
        if (cacheList == null) {
            cacheList = new CacheList();
            cacheLists.put(versionId, cacheList);
        }

        String fileLine = SourceLocation.getFileLine(e.fileName, e.lineNumber);
        boolean checkCache = cacheList.cache(e.applicationVersion, fileLine);

        if (checkCache) {
            //log.debug("Checking in cache again");
            if (sourceLocations.get(e.key) != null) {
                //log.debug("Found in cache");
                return;
            }
        }

        // Add the new location
        //log.debug("Creating New: " + e.key);
        sourceLocations.put(e.key, new SourceLocation(e.applicationVersion, e.fileName, e.lineNumber, e.className, e.methodName));
    }

    private void addResults(List<SourceLocation> list) {
        for (SourceLocation sl : list) {
            ////log.debug("Adding: " + sl.getHashKey());
            sourceLocations.put(sl.getHashKey(), sl);
        }

    }

    private static DBQuery<SourceLocation> getQuery(ApplicationVersion version, String fileLine) {
        return getQuery(version)
                .addWhere("fileLine >= fileLineParam")
                .addParameter("fileLineParam", fileLine);
    }

    private static DBQuery<SourceLocation> getQuery(ApplicationVersion version, String fileLineStart, String fileLineEnd) {
        return getQuery(version, fileLineStart)
                .addWhere("fileLine < fileLineEndParam")
                .addParameter("fileLineEndParam", fileLineEnd);
    }

    private static DBQuery<SourceLocation> getQuery(ApplicationVersion version) {
        return DB.createDBQuery(SourceLocation.class)
                    .addWhere("applicationVersion == applicationVersionParam")
                    .addParameter("applicationVersionParam", version)
                    .setRange(0, LIMIT_PER_QUERY)
                    .addOrderBy("fileLine");
    }

    private class SourceLocationEntry implements Comparable<SourceLocationEntry> {
        String key;
        ApplicationVersion applicationVersion;
        String fileName;
        int lineNumber;
        String className;
        String methodName;

        private SourceLocationEntry(ApplicationVersion applicationVersion, String fileName, int lineNumber, String className, String methodName) {
            this.key = SourceLocation.getHashKey(applicationVersion, fileName, lineNumber, className, methodName);
            this.applicationVersion = applicationVersion;
            this.fileName = fileName;
            this.lineNumber = lineNumber;
            this.className = className;
            this.methodName = methodName;
        }

        public int compareTo(SourceLocationEntry o) {
            return key.compareTo(o.key);
        }
    }

    // 1. Cache the first n entries
    // 2. If the cache size is less than n then we have all entries
    // 3. If not found, cache from the given file:line.

    private class CacheList {
        Cache first;

        public CacheList() {

        }

        public boolean cache(ApplicationVersion version, String fileLine) {
            boolean gotFirst = false;
            //log.debug("Caching " + fileLine);
            if (first == null) {
                //log.debug("Creating initial cache");
                // Get the initial cache
                first = getCache(getQuery(version));
                gotFirst = true;
            }
            Cache cur = first;
            Cache prev = null;
            while (cur != null) {
                //log.debug("Cur = " + cur);
                if (cur.results == 0) {
                    //log.debug("No results, this cache is the end");
                    return false;
                }
                // If we have past our line.
                if (fileLine.compareTo(cur.fileLineStart) < 0) {
                    // The one we are looking for is before the start and considered part of it.
                    if (prev == null) {
                        //log.debug("fileLine < first.fileLineStart");
                        return false;
                    }
                    //log.debug("fileLine < cur.fileLineStart");
                    // There is a gap in our cache.
                    break;
                }
                // The one we are looking for is past the end and considered part of it.
                if (cur.results < LIMIT_PER_QUERY || fileLine.compareTo(cur.fileLineEnd) < 0) {
                    //log.debug("fileLine < cur.fileLineEnd (or in last cache)");
                    return gotFirst;
                }
                prev = cur;
                cur = cur.next;
            }
            if (prev == null) {
                log.error("This should never happen!");
                return false;
            }
            //log.debug("Creating new cache");
            Cache c;
            if (prev.next == null) {
                c = getCache(getQuery(version, fileLine));
            } else {
                c = getCache(getQuery(version, fileLine, prev.next.fileLineStart));
                c.next = prev.next;
            }
            prev.next = c;
            //log.debug("New cache = " + c);
            return c.results > 0;
        }
        
        public Cache getCache(DBQuery<SourceLocation> q) {
            Cache c = new Cache();
            c.addResults(q.getResultList());
            return c;
        }
    }

    private class Cache {
        String fileLineStart;
        String fileLineEnd;
        int results;
        Cache next;

        private Cache() {
        }

        public String toString() {
            return "start = " + fileLineStart + ", end = " + fileLineEnd + ", res = " + results;
        }

        private void addResults(List<SourceLocation> list) {
            //log.debug("Got " + list.size() + " of " + LIMIT_PER_QUERY + " source locations");
            results = list.size();
            if (results > 0) {
                fileLineStart = list.get(0).getFileLine();
                fileLineEnd = list.get(list.size()-1).getFileLine();
            }
            SourceLocationStore.this.addResults(list);
        }
    }

//    public void addSourceLocation(SourceLocation sourceLocation) {
//        sourceLocations.add(sourceLocation);
//    }

//    public void createSourceLocations(List<StackTraceElementDto> stackTrace) {
//        DB.begin();
//        try {
//            for (StackTraceElementDto c : stackTrace) {
//
//            }
//        } finally {
//            DB.commit();
//        }
//    }
}
